/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.commons.lang;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.*;

/** A collection of utilities for encoding URLs. */

public class URLEncodedUtils {

    private static final String PARAMETER_SEPARATOR  = "&";
    private static final String NAME_VALUE_SEPARATOR = "=";

    /**
     * Returns Map<String, Set<String>>  as built from the
     * URI's query portion. For example, a URI of http://example.org/path/to/file?a=1&b=2&c=3&c=4
     * would return a Map three key is a name name of parameter Set<String>
     * is a values, a={1}, one for b={2} and two for c={3,4}.
     * <p/>
     * <p/>
     * This is typically useful while parsing an HTTP PUT.
     *
     * @param uri
     *         uri to parse
     * @param encoding
     *         encoding to use while parsing the query.
     */
    public static Map<String, Set<String>> parse(final URI uri, final String encoding) {
        Map<String, Set<String>> result = Collections.emptyMap();
        final String query = uri.getRawQuery();
        if (query != null && query.length() > 0) {
            result = new HashMap<>();
            parse(result, new Scanner(query), encoding, true);
        }
        return result;
    }

    /**
     * Returns Map<String, Set<String>>  as built from the
     * URI's query portion. Parameters encoding does not performed.
     * For example, a URI of http://example.org/path/to/file?a=1&b=2&c=3&c=4
     * would return a Map three key is a name name of parameter Set<String>
     * is a values, a={1}, one for b={2} and two for c={3,4}.
     * <p/>
     * <p/>
     * This is typically useful while parsing an HTTP PUT.
     *
     * @param uri
     *         uri to parse
     */
    public static Map<String, Set<String>> parse(final URI uri) {
        return parse(uri, true);
    }

    /**
     * Returns Map<String, Set<String>>  as built from the
     * URI's query portion. Parameters encoding does not performed.
     * For example, a URI of http://example.org/path/to/file?a=1&b=2&c=3&c=4
     * would return a Map three key is a name name of parameter Set<String>
     * is a values, a={1}, one for b={2} and two for c={3,4}.
     * <p/>
     * <p/>
     * This is typically useful while parsing an HTTP PUT.
     *
     * @param uri
     *         uri to parse
     * @param decodeQueryParam
     *         decode query parameter or not.
     */
    public static Map<String, Set<String>> parse(final URI uri, boolean decodeQueryParam) {
        Map<String, Set<String>> result = Collections.emptyMap();
        final String query = uri.getRawQuery();
        if (query != null && query.length() > 0) {
            result = new HashMap<>();
            parse(result, new Scanner(query), null, decodeQueryParam);
        }
        return result;
    }


    /**
     * Adds all parameters within the Scanner to the list of
     * <code>parameters</code>, as encoded by <code>encoding</code>.
     * If <code>encoding</code> is null encoding does not performed.
     * For example, a scanner containing the string <code>a=1&b=2&c=3</code> would
     * add the Map<String, Set<String>>  a=1, b=2, and c=3 to the
     * list of parameters.
     *
     * @param parameters
     *         List to add parameters to.
     * @param scanner
     *         Input that contains the parameters to parse.
     * @param encoding
     *         Encoding to use when decoding the parameters. If encoding is null encoding does not performed.
     */
    private static void parse(final Map<String, Set<String>> parameters, final Scanner scanner, final String encoding,
                              boolean decodeQueryParam) {
        scanner.useDelimiter(PARAMETER_SEPARATOR);
        while (scanner.hasNext()) {
            final String[] nameValue = scanner.next().split(NAME_VALUE_SEPARATOR);
            if (nameValue.length == 0 || nameValue.length > 2)
                throw new IllegalArgumentException("bad parameter");

            final String name = decodeQueryParam ? decode(nameValue[0], encoding) : nameValue[0];
            String value = null;
            if (nameValue.length == 2)
                value = decodeQueryParam ? decode(nameValue[1], encoding) : nameValue[1];

            Set<String> values = parameters.get(name);
            if (values == null) {
                values = new LinkedHashSet<>();
                parameters.put(name, values);
            }
            if (value != null) {
                values.add(value);
            }
        }
    }

    /**
     * Returns a String that is suitable for use as an <code>application/x-www-form-urlencoded</code>
     * list of parameters in an HTTP PUT or HTTP POST.
     *
     * @param parameters
     *         The parameters to include.
     * @param encoding
     *         The encoding to use.
     */
    public static String format(final Map<String, Set<String>> parameters, final String encoding) {
        final StringBuilder result = new StringBuilder();

        for (Map.Entry<String, Set<String>> parameter : parameters.entrySet()) {
            final String encodedName = encode(parameter.getKey(), encoding);
            final Set<String> values = parameter.getValue();
            if (values == null || values.size() == 0) {
                result.append(encodedName);
                result.append(NAME_VALUE_SEPARATOR);
                result.append("");
            } else {
                for (String value : values) {
                    final String encodedValue = value != null ? encode(value, encoding) : "";
                    if (result.length() > 0)
                        result.append(PARAMETER_SEPARATOR);
                    result.append(encodedName);
                    result.append(NAME_VALUE_SEPARATOR);
                    result.append(encodedValue);
                }
            }
        }
        return result.toString();
    }

    private static String decode(final String content, final String encoding) {
        try {
            return URLDecoder.decode(content,
                                     encoding != null ? encoding : "UTF-8");
        } catch (UnsupportedEncodingException problem) {
            throw new IllegalArgumentException(problem);
        }
    }

    private static String encode(final String content, final String encoding) {
        try {
            return URLEncoder.encode(content,
                                     encoding != null ? encoding : "UTF-8");
        } catch (UnsupportedEncodingException problem) {
            throw new IllegalArgumentException(problem);
        }
    }

}
